package com.navercorp.utilset.string;

import com.navercorp.utilset.utils.UtilSetLogUtils;

import ohos.utils.net.Uri;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Base64;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**
 * It is easy to be confused by the result that the compressed string is not shorter than original one.<br>
 * When string to be compressed neither lengthy nor repeated at all, then that is not the case for this method to be utilized.
 * Long repeated string is where this method really shines.<p>
 * Look at these examples.<br>
 * A long repeated string 'aaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbcccccccccc' will be compressed into
 * 'H4slAAAAAAAAAEtMxAKSslBkOAAA0lkFljMAAAA=' which is shorter than original one.
 * In contrast, A short but not repeated string 'abcdefghijklmnopqrstuvwxyz' will be compressed into
 * 'H4slAAAAAAAAAEtMSk5JTUvPyMzKzsnNyy8oLCouKS0rr6isAgC9UCdMGgAAAA==' which is longer than original string.
 *
 * @author jaemin.woo
 *
 */
public class StringCompressor {
    private static final String TAG = StringCompressor.class.getSimpleName();

    private static final int BUFFER_SIZE = 2 * 1024;

    private static final int NUMBER_ONE = -1;

    private static final String CHARSET_NAME = "UTF-8";

    /**
     * constructor
     */
    public StringCompressor() {
        /* Do nothing */
    }

    /**
     * compress
     * get the compressed string
     *
     * @param str String
     * @return str
     */
    public String compress(String str) {
        if (str == null || str.length() == 0) {
            return str;
        }

        String compressBase64EncodeStr = null;
        InputStream is = null;
        GZIPOutputStream gzos = null;

        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            is = new ByteArrayInputStream(str.getBytes(CHARSET_NAME));
            gzos = new GZIPOutputStream(baos);
            byte[] buffers = new byte[BUFFER_SIZE];
            int count = NUMBER_ONE;
            while ((count = is.read(buffers, 0, BUFFER_SIZE)) != NUMBER_ONE) {
                gzos.write(buffers, 0, count);
            }
            gzos.finish();
            compressBase64EncodeStr = new String(Base64.getEncoder().encode(baos.toByteArray()));
        } catch (UnsupportedEncodingException e1) {
            // This can't be happened
            UtilSetLogUtils.error(TAG, "UnsupportedEncodingException");
        } catch (IOException e) {
            UtilSetLogUtils.error(TAG, "IOException");
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    UtilSetLogUtils.error(TAG, "IOException");
                }
            }

            if (gzos != null) {
                try {
                    gzos.close();
                } catch (IOException e) {
                    UtilSetLogUtils.error(TAG, "IOException");
                }
            }
        }
        return compressBase64EncodeStr;
    }

    private static String urlDecode(String src) {
        String dst = null;
        try {
            dst = URLDecoder.decode(src, CHARSET_NAME);
        } catch (UnsupportedEncodingException e) {
            UtilSetLogUtils.error("UtilSet", "An UnsupportedEncodingException occured in " + StringCompressor.class
                    + ".urlDecode. However, it does not terminate application and continues "
                    + "decode with default encoding character set.");
            dst = Uri.decode(src);
        }

        return dst;
    }

    /**
     * get the decompressed string
     *
     * @param str String
     * @return str
     */
    public String decompress(String str) {
        if (str == null || str.length() == 0) {
            return str;
        }

        String decompressStr = null;
        InputStream is = null;
        GZIPInputStream gzis = null;
        ByteArrayOutputStream baos = null;

        try {
            is = new ByteArrayInputStream(Base64.getDecoder().decode(urlDecode(str)));

            gzis = new GZIPInputStream(is);
            baos = new ByteArrayOutputStream();

            byte[] buffers = new byte[BUFFER_SIZE];
            int count = NUMBER_ONE;
            while ((count = gzis.read(buffers, 0, BUFFER_SIZE)) != NUMBER_ONE) {
                baos.write(buffers, 0, count);
            }

            decompressStr = new String(baos.toByteArray(), CHARSET_NAME);
        } catch (IOException e) {
            UtilSetLogUtils.error(TAG, "IOException");
        } finally {
            if (baos != null) {
                try {
                    baos.close();
                } catch (IOException e) {
                    UtilSetLogUtils.error(TAG, "IOException");
                }
            }

            if (gzis != null) {
                try {
                    gzis.close();
                } catch (IOException e) {
                    UtilSetLogUtils.error(TAG, "IOException");
                }
            }
        }
        return decompressStr;
    }

    /**
     * get the InputStream
     *
     * @param is InputStream
     * @return str
     */
    public InputStream decompress(InputStream is) {
        if (is == null) {
            return is;
        }

        InputStream retIs = null;
        GZIPInputStream gzis = null;
        ByteArrayOutputStream baos = null;

        try (ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().
                decode(convertStreamToByteArray(is)))) {
            gzis = new GZIPInputStream(bais);
            baos = new ByteArrayOutputStream();

            byte[] buffers = new byte[BUFFER_SIZE];
            int count = NUMBER_ONE;
            while ((count = gzis.read(buffers, 0, BUFFER_SIZE)) != NUMBER_ONE) {
                baos.write(buffers, 0, count);
            }

            retIs = new ByteArrayInputStream(baos.toByteArray());
        } catch (IOException e) {
            UtilSetLogUtils.error(TAG, "IOException");
        } finally {
            if (baos != null) {
                try {
                    baos.close();
                } catch (IOException e) {
                    UtilSetLogUtils.error(TAG, "IOException");
                }
            }

            if (gzis != null) {
                try {
                    gzis.close();
                } catch (IOException e) {
                    UtilSetLogUtils.error(TAG, "IOException");
                }
            }
        }
        return retIs;
    }

    private byte[] convertStreamToByteArray(InputStream is) throws IOException {
        StringBuilder sb = new StringBuilder();
        String line = null;

        try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            // Just bypasses exception so that the caller can handle finalization works or propagate exception
            UtilSetLogUtils.error(TAG, "IOException");
        } finally {
            try {
                is.close();
            } catch (IOException e) {
                // This barely happens.
                // Just bypasses exception so that the caller can handle finalization works or propagate exception
                UtilSetLogUtils.error(TAG, "IOException");
            }
        }
        return sb.toString().getBytes();
    }
}